package com.airbnb.epoxy; import com.airbnb.epoxy.EpoxyAttribute.Option; import com.squareup.javapoet.AnnotationSpec; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.TypeName; import java.lang.annotation.ElementType; import java.lang.annotation.Target; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import static com.airbnb.epoxy.Utils.capitalizeFirstLetter; import static com.airbnb.epoxy.Utils.isFieldPackagePrivate; import static com.airbnb.epoxy.Utils.startsWithIs; import static javax.lang.model.element.Modifier.FINAL; import static javax.lang.model.element.Modifier.PRIVATE; import static javax.lang.model.element.Modifier.STATIC; class BaseModelAttributeInfo extends AttributeInfo { private final TypeElement classElement; protected Types typeUtils; BaseModelAttributeInfo(Element attribute, Types typeUtils, Elements elements, ErrorLogger errorLogger) { this.typeUtils = typeUtils; this.name = attribute.getSimpleName().toString(); this.typeName = TypeName.get(attribute.asType()); typeMirror = attribute.asType(); classElement = (TypeElement) attribute.getEnclosingElement(); modelName = classElement.getSimpleName().toString(); modelPackageName = elements.getPackageOf(classElement).getQualifiedName().toString(); this.hasSuperSetter = hasSuperMethod(classElement, attribute); this.hasFinalModifier = attribute.getModifiers().contains(FINAL); this.packagePrivate = isFieldPackagePrivate(attribute); EpoxyAttribute annotation = attribute.getAnnotation(EpoxyAttribute.class); Set<Option> options = new HashSet<>(Arrays.asList(annotation.value())); validateAnnotationOptions(errorLogger, annotation, options); //noinspection deprecation useInHash = annotation.hash() && !options.contains(Option.DoNotHash); ignoreRequireHashCode = options.contains(Option.IgnoreRequireHashCode); generateSetter = annotation.setter() && !options.contains(Option.NoSetter); generateGetter = !options.contains(Option.NoGetter); isPrivate = attribute.getModifiers().contains(PRIVATE); if (isPrivate) { findGetterAndSetterForPrivateField(errorLogger); } buildAnnotationLists(attribute.getAnnotationMirrors()); } /** * Check if the given class or any of its super classes have a super method with the given name. * Private methods are ignored since the generated subclass can't call super on those. */ protected boolean hasSuperMethod(TypeElement classElement, Element attribute) { if (!Utils.isEpoxyModel(classElement.asType())) { return false; } for (Element subElement : classElement.getEnclosedElements()) { if (subElement.getKind() == ElementKind.METHOD) { ExecutableElement method = (ExecutableElement) subElement; if (!method.getModifiers().contains(Modifier.PRIVATE) && method.getSimpleName().toString().equals(attribute.getSimpleName().toString()) && method.getParameters().size() == 1 && method.getParameters().get(0).asType().equals(attribute.asType())) { return true; } } } Element superClass = typeUtils.asElement(classElement.getSuperclass()); return (superClass instanceof TypeElement) && hasSuperMethod((TypeElement) superClass, attribute); } private void validateAnnotationOptions(ErrorLogger errorLogger, EpoxyAttribute annotation, Set<Option> options) { if (options.contains(Option.IgnoreRequireHashCode) && options.contains(Option.DoNotHash)) { errorLogger .logError("Illegal to use both %s and %s options in an %s annotation. (%s#%s)", Option.DoNotHash, Option.IgnoreRequireHashCode, EpoxyAttribute.class.getSimpleName(), classElement.getSimpleName(), name); } // Don't let legacy values be mixed with the new Options values if (!options.isEmpty()) { if (!annotation.hash()) { errorLogger .logError("Don't use hash=false in an %s if you are using options. Instead, use the" + " %s option. (%s#%s)", EpoxyAttribute.class.getSimpleName(), Option.DoNotHash, classElement.getSimpleName(), name); } if (!annotation.setter()) { errorLogger .logError("Don't use setter=false in an %s if you are using options. Instead, use the" + " %s option. (%s#%s)", EpoxyAttribute.class.getSimpleName(), Option.NoSetter, classElement.getSimpleName(), name); } } } /** * Checks if the given private field has getter and setter for access to it */ private void findGetterAndSetterForPrivateField(ErrorLogger errorLogger) { for (Element element : classElement.getEnclosedElements()) { if (element.getKind() == ElementKind.METHOD) { ExecutableElement method = (ExecutableElement) element; String methodName = method.getSimpleName().toString(); // check if it is a valid getter if ((methodName.equals(String.format("get%s", capitalizeFirstLetter(name))) || methodName.equals(String.format("is%s", capitalizeFirstLetter(name))) || (methodName.equals(name) && startsWithIs(name))) && !method.getModifiers().contains(PRIVATE) && !method.getModifiers().contains(STATIC) && method.getParameters().isEmpty() && TypeName.get(method.getReturnType()).equals(typeName)) { getterMethodName = methodName; } // check if it is a valid setter if ((methodName.equals(String.format("set%s", capitalizeFirstLetter(name))) || (startsWithIs(name) && methodName.equals(String.format("set%s", name.substring(2, name.length()))))) && !method.getModifiers().contains(PRIVATE) && !method.getModifiers().contains(STATIC) && method.getParameters().size() == 1 && TypeName.get(method.getParameters().get(0).asType()).equals(typeName)) { setterMethodName = methodName; } } } if (getterMethodName == null || setterMethodName == null) { errorLogger .logError("%s annotations must not be on private fields" + " without proper getter and setter methods. (class: %s, field: %s)", EpoxyAttribute.class, classElement.getSimpleName(), name); } } /** * Keeps track of annotations on the attribute so that they can be used in the generated setter * and getter method. Setter and getter annotations are stored separately since the annotation may * not target both method and parameter types. */ private void buildAnnotationLists(List<? extends AnnotationMirror> annotationMirrors) { for (AnnotationMirror annotationMirror : annotationMirrors) { if (!annotationMirror.getElementValues().isEmpty()) { // Not supporting annotations with values for now continue; } ClassName annotationClass = ClassName.bestGuess(annotationMirror.getAnnotationType().toString()); if (annotationClass.equals(ClassName.get(EpoxyAttribute.class))) { // Don't include our own annotation continue; } DeclaredType annotationType = annotationMirror.getAnnotationType(); // A target may exist on an annotation type to specify where the annotation can // be used, for example fields, methods, or parameters. Target targetAnnotation = annotationType.asElement().getAnnotation(Target.class); // Allow all target types if no target was specified on the annotation List<ElementType> elementTypes = Arrays.asList(targetAnnotation == null ? ElementType.values() : targetAnnotation.value()); AnnotationSpec annotationSpec = AnnotationSpec.builder(annotationClass).build(); if (elementTypes.contains(ElementType.PARAMETER)) { setterAnnotations.add(annotationSpec); } if (elementTypes.contains(ElementType.METHOD)) { getterAnnotations.add(annotationSpec); } } } }